from djcontrol import *

from Tkinter import *
import djcontrolutil

# note minor validation bug. If you set validation to numeric, select something, and press a letter, the text dissappears.

class DjInput(DjControl):
	"""
	Input, based on Tkinter Entry field.
	
	Properties:
	
	*text* - String. Current contents of the input.
	*bgcolor* - Background color of the input. RGB triplet, such as (255,0,0) for pure red.
	*color* - Foreground color of the input. RGB triplet.
	*borderwidth* - An integer, width of border in pixels.
	*font* - Set font of input. Standard tkinter font format, such as ('Times New Roman', 12, 'normal')
	*alignment* - Set alignment of text. One of 'left','right','center'.
	*borderstyle* - Border style. One of 'sunken','raised','groove','ridge','flat'. Default is 'sunken'.
	
	*selectioncolor* - Color of selected text. RGB triplet.
	*selectionbgcolor* - Background color of selected text. RGB triplet.
	*password* - True or false. Set to True to display all characters as *.
	
	*width* - Set width of input, where one unit is the width of a char. Default is 20.
	
	*enabled* -  If False, the input is disabled, and no text can be inserted (even by code). Takes precedence over readonly.
	*readonly* - If True, the input is read only, but text can still be selected and copied. Has no visible effect if enabled is false.
	"""
	
	# Note that only names beginning with '_', or the special case basecontrol, should be used.
	_autoProperties = None
	_readonly = False
	_enabled = True
	basecontrol = 0
	
	def __init__(self, parent, strDefaultText='', **options):
		parent = djcontrolutil.getbasecontrol(parent)
		self.basecontrol = Entry(parent)
		self._autoProperties = {
		# Property name,  Tkinter property name, SET wrapper, GET wrapper
		'bgcolor': ('background',djcontrolutil.colorToString,djcontrolutil.stringToColor),
		'color': ('foreground',djcontrolutil.colorToString,djcontrolutil.stringToColor),
		'selectioncolor': ('selectforeground',djcontrolutil.colorToString,djcontrolutil.stringToColor),
		'selectionbgcolor': ('selectbackground',djcontrolutil.colorToString,djcontrolutil.stringToColor),
		'borderwidth': ('borderwidth',None,None),
		'font': ('font',None,None),
		'alignment': ('justify',djcontrolutil.stringToJustify,djcontrolutil.justifyToString),
		'borderstyle': ('relief', djcontrolutil.stringToRelief,djcontrolutil.reliefToString),
			
		'password': ('show', _boolToShowPassword,_showPasswordToBool),
		'width': ('width',None,None),		
		}
		if strDefaultText!='': self.text=strDefaultText
		self.set_many(**options)
	
	def scrollto_start(self):
		"""Scroll to the start of the input field."""
		self.basecontrol.xview(0)
	
	def scrollto_end(self):
		"""Scroll to the end of the input field."""
		self.basecontrol.xview(END)
	
	def get_selected_text(self):
		"""Return the text that is selected. "" (empty string) if there is no selection."""
		if self.basecontrol.selection_present():
			lower = min(self.basecontrol.index(ANCHOR), self.basecontrol.index(INSERT))
			upper = max(self.basecontrol.index(ANCHOR), self.basecontrol.index(INSERT))
			return self.basecontrol.get()[lower:upper]
		else:
			return ''
			
	def get_selection(self):
		"""Return the indices of the selection. Use get_selected_text to retrieve the actual text. Position of cursor if there is no selection."""
		if self.basecontrol.selection_present():
			lower = min(self.basecontrol.index(ANCHOR), self.basecontrol.index(INSERT))
			upper = max(self.basecontrol.index(ANCHOR), self.basecontrol.index(INSERT))
			return (lower, upper)
		else:
			insert = self.basecontrol.index(INSERT)
			return (insert, insert)
	
	def select_all_binding(self):
		"""Add Ctrl+A -> select all binding"""
		def sel(self):
			self.select_all()
			return 'break'
		self.bind('<Control-a>',lambda event: sel(self), '+')
	
	def select_all(self):
		"""Select all text in the input."""
		self.select_range(0,END)
	
	def select_range(self, start, stop=END):
		"""Select range of text in the input."""
		# Don't use selection_range because for some reason Tkinter doesn't set the flags correctly
		self.basecontrol.selection_from(start)
		self.basecontrol.selection_to(stop)
		#For some reason, tkinter doesn't also set the insertion cursor.
		self.basecontrol.icursor(stop)
	def deselect(self):
		"""Deselect any selection in the input"""
		self.basecontrol.selection_clear()
	
	def delete_text(self, start, stop=END):
		"""Delete range of text."""
		self.basecontrol.delete(start, stop)
	
	def clear(self):
		"""Delete all text."""
		self.basecontrol.delete(0, END)
	
	def insert_text(self, strText, bEnd=False):
		"""By default, inserts text into the field at the current position, replacing any current selection. If bEnd is True, appends text to the end of the field."""
		if bEnd:
			self.basecontrol.insert(END, strText)
		else:
			if self.basecontrol.selection_present():
				lower,upper = self.get_selection() # Get indices for selection
				self.basecontrol.delete(lower, upper)
				self.basecontrol.insert(INSERT, strText)
			else:
				self.basecontrol.insert(INSERT, strText)
	
	def enable_validation(self, validate, whenValidate='all', fnInvalid=None):
		"""Add validation to only allow certain characters in the input field. validate can either be the string 'numeric', a string in the form 'maxlength:3', or a custom function returning True or False. The custom function should take a string as an argument. whenValidate is one of 'focus','focusin','focusout','all','none'. If whenValidate is at focus, you will want to provide a fnInvalid function that will be called if the value is not valid. Use disable_validation to disable validation."""
		if isinstance(validate, str):
			if validate.startswith('maxlength:'):
				nMaxLength = int(validate[len('maxlength:'):])
				def fnValidate(s, maxLength=nMaxLength):
					return len(s) <= maxLength
			elif validate=='numeric':
				def fnValidate(s):
					for c in s:
						if c not in '0123456789.':
							return False
					return True
			else:
				raise RuntimeError, 'Validate must either be custom function, the string "numeric", or a string in the form "maxlength:3"'
		else:
			fnValidate = validate #We were (in theory) passed a custom function
		
		if fnInvalid: self.basecontrol.config(invalidcommand=fnInvalid)
		# Test to check if the function accepts the correct number of arguments.
		fnValidate('test')
		# This is kind of tricky. This registers the function with Tkinter so that it performs variable subsitution.
		# See http://mail.python.org/pipermail/tkinter-discuss/2006-August/000863.html
		fnValidate = (self.basecontrol.register(fnValidate), '%P')
		self.basecontrol.config(validatecommand=fnValidate, validate=whenValidate)
		
	def disable_validation(self):
		"""Turn off validation for the input field."""
		self.basecontrol.config(validate = 'none')
	
	_customProperties = ('text','enabled','readonly')
	def _lookupCustomProperties(self, bSet, strAttribute, value=None):
		if strAttribute=='text':
			if bSet:
				self.basecontrol.delete(0, END)
				self.basecontrol.insert(END, value)
			else:
				return self.basecontrol.get()
		elif strAttribute=='enabled':
			# This takes precedence over readonly.
			if bSet:
				self._enabled = value
				if value:
					self.basecontrol.config(state=DISABLED)
				else:
					if self._readonly: self.basecontrol.config(state='readonly')
					else: self.basecontrol.config(state='normal')
			else:
				return self._enabled
		elif strAttribute=='readonly':
			if bSet:
				self._readonly = value
				if not self.enabled: #Has no visible effect if the input is disabled.
					return
				# Retain normal background color if readonly
				if value: self.basecontrol.config(state='readonly',readonlybackground='SystemWindow')
				else:	self.basecontrol.config(state=NORMAL)
			else:
				return self._readonly
		else:
			raise AttributeError, strAttribute
	



def _boolToShowPassword(value):
	if value: return '*'
	else: return ''
def _showPasswordToBool(value):
	if value: return True #i.e., the value is not empty, so return true.
	else: return False
